package scuts.traits;

typedef TopLevelType = Type;

import haxe.macro.Expr;
import haxe.Stack;


#if (macro || display)
import scuts.macro.Convert;
import haxe.macro.Context;
import scuts.macro.ExtendedContext;
import scuts.Assert;
import scuts.macro.Parse;
import scuts.macro.Print;

typedef SType = scuts.macro.Type;

import haxe.macro.Type;


using scuts.Core;
//using scuts.ArrayTools;
#end
//#if !macro extern #end
class Traits {
	
	static var fields:Hash<{mixinClass:haxe.macro.Type.ClassType, fields:Array<Field>}> = new Hash();
	
	static var alreadyAdded:Hash<Bool> = new Hash();
	
	
	#if !macro @:macro #end
	public static function register (?addAutoBuild:Bool = true):Array<Field> {
		trace("start register");
		
		var curFields = haxe.macro.Context.getBuildFields();
		
		
		
		
		var lc = haxe.macro.Context.getLocalClass();
		
		
			if (addAutoBuild) {
			var s = "scuts.traits.Traits.apply(" + lc.get().module + "." + lc.get().name + ")";
			trace(s);
			
			lc.get().meta.add(":autoBuild", [Parse.parse(s)], Context.currentPos());
			}
		
		var isInterfaceTrait = lc.get().isInterface;
		//trace("isInterface: " + isInterfaceTrait);
		//trace(lc.get().params);
		//lc.get().exclude();
		var fullName = SType.getFullQualifiedTypeName(lc.get());
		var res = [];
		
		for (f in curFields) {
			switch (f.kind) {
				
				case FVar( t , e ): 
					if (!f.access.any(function (a) return a == APrivate)) {
						res.push(f);
					}
					f.meta.insert(0, { pos:Context.currentPos(), params:[], name:":fromTrait" } );
				case FFun( fun ): 
					var newExpr = isInterfaceTrait ? null : Parse.parse("throw 'abstract trait implementation'");
					var metaCopy = f.meta.copy();
          // mark this function to handle them properly in subtypes
					f.meta.insert(0, { pos:Context.currentPos(), params:[], name:":fromTrait" } );
          // its an interface, so remove constructor calls and method bodies
					if (f.name != "new") {
            res.push( {
              access: f.access.copy(),
              doc: f.doc,
              kind: FFun({ expr:newExpr, args: fun.args, params:fun.params, ret:fun.ret}),
              meta: metaCopy,
              name:f.name,
              pos:f.pos
            });
					}
					
					if (!isInterfaceTrait) {
						var hasOverride = f.access.any(
              function (a) {
                return switch (a) {
                  case AOverride: true;
                  default: false;
                }
              }
            );
							
						
						if (!hasOverride) {
							//f.access.push(AOverride);
						}
					}
					
				case FProp( get , set , t, null ):
					//trace("prop: " + t);
					res.push(f);
			}
		}
		
		//trace(fullName);
		
		//trace("here");
		fields.set(fullName, { mixinClass:lc.get(), fields:curFields } );
		trace("end register");
		return res;
	}
	
	#if (macro || display)
	public static function hasSuperField (lc:ClassType, fieldName:String):Bool {
		var sc = lc.superClass;
		while (sc != null) {
			for (f in sc.t.get().fields.get()) {
				if (f.name == fieldName) return true;
			}
			sc = sc.t.get().superClass;
		}
		return false;
	}
	
	public static function removeDuplicateFields (a:Array<Field>, f: Array<Field>->Null<Field>):Array<Field>
	{
		var res = [];
		
		var fieldHash = new Hash<Array<Field>>();
		for (f in a) {
			var a:Array<Field> = if (!fieldHash.exists(f.name)) {
				var arr = [];
				fieldHash.set(f.name, arr);
				arr;
				
			} else {
				fieldHash.get(f.name);
			}
			a.push(f);
		}
		
		for (dupFields in fieldHash) {
			if (dupFields.length > 1) {
				var r = f(dupFields);
				if (r != null) {
					res.push(r);
				}
			} else {
				res.push(dupFields[0]);
			}
		}
		return res;
		
	}
	
	public static function findInterfaceParams(interf:ClassType, localClass:ClassType):Array<Type> {
		
		trace("INTERFACE: " + interf.name);
		trace("INTERFACE PARAMS: " + interf.params);
		trace("LOCAL CLASS: " + localClass.name);
		trace("INTERFACE PARAMS: " + localClass.params);
		var res = findInterfaceParams1(interf, localClass, localClass.params, []);
    trace("INTERFACE PARAMS AFTER: " + localClass.params);
    trace("found: " + res);
    return res;
	}
	
	public static function findInterfaceParams1(interf:ClassType, localClass:ClassType, globalParams:Array<{name:String, t:Type}>, curParams:Array<Type>):Array<Type> {
		
		
		for (l in localClass.interfaces) {
			
			if (l.t.get().name == interf.name && l.t.get().module == interf.module) {
				trace("INTERFACE FOUND: " + interf.name);
				//trace(curParams);
				//trace(l.params);
				return l.params;
				
			} else {
				
				trace("GETTING DEEPER: " + l.t.get().name);
				
				trace("PARAMS EXPLICIT: " + l.params);
				trace("PARAMS GENERIC: " + l.t.get().params);
				var r = findInterfaceParams1(interf, l.t.get(), globalParams, l.params);
				if (r != null) {
					r = r.map(function (t) {
						return switch (t) {
							case TInst(t1, params):
								var val = l.t.get().params.findIndex(function (e) 
								{
									return switch (e.t) {
										case TInst(t2, params2):
											if (t1.toString() == t2.toString()) {
												true;
											}
										default:
											false;
									}
									return false;
								});
								return switch (val) {
									case Some(v):
										l.params[v];
									case None:
										t;
										
								}
							default:
								t;
						}
					});
				
					trace("RESULT: " + r);
				
					return r;
				}
			}
		}
		return null;
	}
	
	#end
	#if !macro @:macro #end
	public static function apply (mixin:ExprRequire<Class<Dynamic>>):Array<Field> {
		
    var curFields = Context.getBuildFields();
		var str = "{ var a: $0 = null; a; }";
		var localClass = Context.getLocalClass().get();
		
		
		var type = SType.getType(Print.exprStr(mixin));
		
		
		
		var info = switch (type) {
			case TInst(t, _): { fullName: SType.getFullQualifiedTypeName(t.get()), t:t.get() };
			
			default: Context.error("Invalid Type: " + Print.exprStr(mixin), Context.currentPos());
		}
		
		var id = Context.signature( localClass.pack + localClass.module + localClass.name + 
									info.fullName);
		if (alreadyAdded.exists(id)) {
			trace("already done");
			return curFields;
		} else {
			
			alreadyAdded.set(id, true);
		}
		
		trace("--------------  start apply: " + SType.getFullQualifiedTypeName(localClass) + " with " + info.fullName + " ------------");
		
		
		var localClassRef = Context.getLocalClass();
		
		
		if (localClass.isInterface) {
			return curFields;
		}
		//var superAutoBuilds = localClass.superClass.t.get().meta.get().filter(function (m) return m.name == ":autoBuild");
		
		
		//trace(localClass);
		
		
		
		
		//trace(info.t.superClass);
		if (info.t.superClass != null) {
			if (localClassRef.get().superClass == null) {
				Context.error("Class must extend " + info.t.superClass.t.toString(), Context.currentPos());
				return [];
			}
			var sClass = localClassRef.get().superClass;
			var found = false;
			var traitId = Std.string(info.t.superClass.params) + Std.string(info.t.superClass.t);
			while (sClass != null && found == false) {
				var id = Std.string(sClass.params) + Std.string(sClass.t);
				if (id == traitId) found = true;
				sClass = sClass.t.get().superClass;
			}
			if (!found) {
				Context.error("Class must extend " + info.t.superClass.t.toString(), Context.currentPos());
			}
		}
		
		var traitHasParams = info.t.params.length > 0;
		var traitParamCount = info.t.params.length;
		trace("------------START FIND--------");
		var params = findInterfaceParams(info.t, localClass);
		var paramReplacements2 = if (params == null) [] else params;
		trace(paramReplacements2);
		trace("------------END FIND--------");
		
		//trace(paramReplacements2);
		
		
		
		try {
		
		
		var paramReplacements = paramReplacements2.map(function (e) return e);
		//trace(paramReplacements);
		//trace(info.t.params);
		var paramsZipped = paramReplacements.zip(info.t.params);
		//trace(paramsZipped);
		var paramLookup = new Hash();
		
		for (p in paramsZipped) {
			paramLookup.set(p._2.name, p);
		}
		
		
		
		trace(paramLookup);
		
		
			
		
		if (!fields.exists(info.fullName)) {
			// load type
			//trace("now");
			var curType = SType.getTypeFromModule(info.t.module, info.t.name);
			//trace("after now");
			//trace(info.fullName);
			Assert.assertTrue(fields.exists(info.fullName));
		}
		// modify fields
		var mixinFieldsBase = fields.get(info.fullName).fields.copy();
		var mixinParams = fields.get(info.fullName).mixinClass.params;
		var mixinFields = if (false && !info.t.isInterface && info.fullName != SType.getFullQualifiedTypeName(localClass.superClass.t.get())) {
			[];
		} else {
			var mixinFields2 = [];
			for (m in mixinFieldsBase) {
				if (!curFields.any(function (e) return e.name == m.name) && !hasSuperField(localClass, m.name)) {
					mixinFields2.push(m);
				}
				
			}
			mixinFields2;
		}
		
		
		
		
		mixinFields = mixinFields.map(function (f) {
			return switch (f.kind) {
				case FieldType.FVar(t, e):
					var typeString = Print.complexTypeStr(t);
					if (paramLookup.exists(typeString)) {
						{
							access: f.access,
							doc: f.doc, 
							kind: FVar(Convert.typeToComplexType(paramLookup.get(typeString)._1, Context.currentPos()), e),
							meta: f.meta,
							name: f.name, 
							pos: f.pos
						};
					} else {
						f;
					}
				case FieldType.FFun(fun):
					var args = fun.args.map( function (a) {
						var type = 
							if (Print.complexTypeStr(a.type).split(".").last() == info.t.name) // type param
							{
                //trace("HERE WITH " + a.type);
								var res = Convert.stringToComplexType(SType.getFullQualifiedTypeName(localClass), Context.currentPos());
                trace("HERE WITH " + res);
                res;
							} else if (paramLookup.exists(Print.complexTypeStr(a.type))) {
								var s = Print.complexTypeStr(a.type);
                var t = paramLookup.get(s)._1;
                
                var res = Convert.typeToComplexType(t, Context.currentPos());
                trace("HERE WITH " + res);
                trace("HERE WITH " + Print.complexTypeStr(res));
                res;
							} else 
                {
                 trace("HERE WITH " + a.type);
								a.type;
                }
						
						return {
							type: type,
							name: a.name,
							opt:a.opt,
							value:a.value
						}
					});
					
					var ret = 
						if (fun.ret != null && Print.complexTypeStr(fun.ret).split(".").last() == "This") {
              
							Convert.stringToComplexType(SType.getFullQualifiedTypeNameWithParams(localClass), Context.currentPos());
						} else {
							if (fun.ret != null && Print.complexTypeStr(fun.ret).split(".").last() == info.t.name) 
							{
                
								fun.ret = Convert.stringToComplexType(SType.getFullQualifiedTypeName(localClass), Context.currentPos());
							} else if (fun.ret != null && paramLookup.exists(Print.complexTypeStr(fun.ret))) {
								
                var s = Print.complexTypeStr(fun.ret);
								Convert.typeToComplexType(paramLookup.get(s)._1, Context.currentPos());
							} else {
								
                fun.ret;
              }
						}
          trace("here");
					var newFun = 
						{
							args: args,
							expr: fun.expr,
							params: fun.params,
							ret: ret
						};
					{
						access: f.access,
						doc: f.doc, 
						kind: FFun(newFun),
						meta: f.meta,
						name: f.name, 
						pos: f.pos
						
					};
				default: 
			}
		});
		
		//trace(mixinFields);
		var res = curFields.concat(mixinFields);
		
		//trace("mixinFields: " + mixinFields.mapA(function (f) return f.name));
		//trace("curFields: " + curFields.mapA(function (f) return f.name));
		//trace("Fields before: " + res.mapA(function (f) return f.name));
		
		// priority = no meta (must be set to @:traitOverride), @:traitOverride, @:fromTrait
		res = removeDuplicateFields(res, function(fields) {
			//trace("______________________________");
			var fieldName = fields[0].name;
			var sorted = fields.copy();
			//trace(sorted.mapA(function (f) return f.meta + " - " + f.name));
			
			sorted.sort(function (f1, f2) {
				var f1HasFromTrait = f1.meta.any(function (m) return m.name == ":fromTrait");
				var f2HasFromTrait = f2.meta.any(function (m) return m.name == ":fromTrait");
				var f1HasNothing = !f1HasFromTrait;
				var f2HasNothing = !f2HasFromTrait;
				
				return (f1HasNothing && !f2HasNothing) ? -1 :
					   (f2HasNothing && !f1HasNothing) ? 1 :
					   (f1HasFromTrait && !f2HasFromTrait) ? -1 :
					   (f2HasFromTrait && !f1HasFromTrait)? 1 : 
						{
							throw new Error("You need to make the usage of function " + f1.name + " explicit", Context.currentPos());
							-1;
						};
				
			});
			//var isFromTrait = sorted[0].meta.existsF(function (m) return m.name == ":fromTrait");
			
			//trace(sorted[0].meta);
			
			//var hasOverride = sorted[0].meta.existsF(function (m) return m.name == ":traitOverride");
			/*
			if (isFromTrait && hasSuperField(localClass, fieldName)) {
				trace("YES HAS SUPER OR FROM TRAIT");
				return null;
			} 
			*/
			return sorted[0];
		});
		
		//trace("Fields after: " + res.mapA(function (f) return f.name));
		
    trace("\n\n\nAll Fields for: " + localClass.module + "." + localClass.name + "\n");
		for (f in res) {
			
			//trace("------------------------------");
			trace("\n" + Print.fieldStr(f));
			//trace("------------------------------");
		}
		trace("----------- end apply -------------");
		//trace(res);
		return res;
		}
		catch (e:String) {
      throw "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! " + e;
      trace(e);
			trace("Error: " + haxe.Stack.toString(Stack.callStack()));
			return [];
		}
		
		//fields.concat(MixinRegistry.fields);
		
	}

	
	
}